package org.rrd4j.graph;

import java.awt.*;
import java.awt.image.PixelGrabber;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Vector;

class GifEncoder {
    private Dimension dispDim = new Dimension(0, 0);
    private GifColorTable colorTable;
    private int bgIndex = 0;
    private int loopCount = 1;
    private String theComments;
    private Vector<Gif89Frame> vFrames = new Vector<Gif89Frame>();

    GifEncoder() {
        colorTable = new GifColorTable();
    }

    GifEncoder(Image static_image) throws IOException {
        this();
        addFrame(static_image);
    }

    GifEncoder(Color[] colors) {
        colorTable = new GifColorTable(colors);
    }

    GifEncoder(Color[] colors, int width, int height, byte ci_pixels[])
            throws IOException {
        this(colors);
        addFrame(width, height, ci_pixels);
    }

    int getFrameCount() {
        return vFrames.size();
    }

    Gif89Frame getFrameAt(int index) {
        return isOk(index) ? vFrames.elementAt(index) : null;
    }

    void addFrame(Gif89Frame gf) throws IOException {
        accommodateFrame(gf);
        vFrames.addElement(gf);
    }

    void addFrame(Image image) throws IOException {
        addFrame(new DirectGif89Frame(image));
    }

    void addFrame(int width, int height, byte ci_pixels[])
            throws IOException {
        addFrame(new IndexGif89Frame(width, height, ci_pixels));
    }

    void insertFrame(int index, Gif89Frame gf) throws IOException {
        accommodateFrame(gf);
        vFrames.insertElementAt(gf, index);
    }

    void setTransparentIndex(int index) {
        colorTable.setTransparent(index);
    }

    void setLogicalDisplay(Dimension dim, int background) {
        dispDim = new Dimension(dim);
        bgIndex = background;
    }

    void setLoopCount(int count) {
        loopCount = count;
    }

    void setComments(String comments) {
        theComments = comments;
    }

    void setUniformDelay(int interval) {
        for (int i = 0; i < vFrames.size(); ++i)
            vFrames.elementAt(i).setDelay(interval);
    }

    void encode(OutputStream out) throws IOException {
        int nframes = getFrameCount();
        boolean is_sequence = nframes > 1;
        colorTable.closePixelProcessing();
        Put.ascii("GIF89a", out);
        writeLogicalScreenDescriptor(out);
        colorTable.encode(out);
        if (is_sequence && loopCount != 1)
            writeNetscapeExtension(out);
        if (theComments != null && theComments.length() > 0)
            writeCommentExtension(out);
        for (int i = 0; i < nframes; ++i)
            vFrames.elementAt(i).encode(
                    out, is_sequence, colorTable.getDepth(), colorTable.getTransparent()
            );
        out.write((int) ';');
        out.flush();
    }

    private void accommodateFrame(Gif89Frame gf) throws IOException {
        dispDim.width = Math.max(dispDim.width, gf.getWidth());
        dispDim.height = Math.max(dispDim.height, gf.getHeight());
        colorTable.processPixels(gf);
    }

    private void writeLogicalScreenDescriptor(OutputStream os) throws IOException {
        Put.leShort(dispDim.width, os);
        Put.leShort(dispDim.height, os);
        os.write(0xf0 | colorTable.getDepth() - 1);
        os.write(bgIndex);
        os.write(0);
    }


    private void writeNetscapeExtension(OutputStream os) throws IOException {
        os.write((int) '!');
        os.write(0xff);
        os.write(11);
        Put.ascii("NETSCAPE2.0", os);
        os.write(3);
        os.write(1);
        Put.leShort(loopCount > 1 ? loopCount - 1 : 0, os);
        os.write(0);
    }


    private void writeCommentExtension(OutputStream os) throws IOException {
        os.write((int) '!');
        os.write(0xfe);
        int remainder = theComments.length() % 255;
        int nsubblocks_full = theComments.length() / 255;
        int nsubblocks = nsubblocks_full + (remainder > 0 ? 1 : 0);
        int ibyte = 0;
        for (int isb = 0; isb < nsubblocks; ++isb) {
            int size = isb < nsubblocks_full ? 255 : remainder;
            os.write(size);
            Put.ascii(theComments.substring(ibyte, ibyte + size), os);
            ibyte += size;
        }
        os.write(0);
    }


    private boolean isOk(int frame_index) {
        return frame_index >= 0 && frame_index < vFrames.size();
    }
}

class DirectGif89Frame extends Gif89Frame {
    private int[] argbPixels;

    DirectGif89Frame(Image img) throws IOException {
        PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, true);
        String errmsg = null;
        try {
            if (!pg.grabPixels())
                errmsg = "can't grab pixels from image";
        }
        catch (InterruptedException e) {
            errmsg = "interrupted grabbing pixels from image";
        }
        if (errmsg != null)
            throw new IOException(errmsg + " (" + getClass().getName() + ")");
        theWidth = pg.getWidth();
        theHeight = pg.getHeight();
        argbPixels = (int[]) pg.getPixels();
        ciPixels = new byte[argbPixels.length];
    }

    DirectGif89Frame(int width, int height, int argb_pixels[]) {
        theWidth = width;
        theHeight = height;
        argbPixels = new int[theWidth * theHeight];
        System.arraycopy(argb_pixels, 0, argbPixels, 0, argbPixels.length);
        ciPixels = new byte[argbPixels.length];
    }

    Object getPixelSource() {
        return argbPixels;
    }
}


class GifColorTable {
    private int[] theColors = new int[256];
    private int colorDepth;
    private int transparentIndex = -1;
    private int ciCount = 0;
    private ReverseColorMap ciLookup;

    GifColorTable() {
        ciLookup = new ReverseColorMap();
    }

    GifColorTable(Color[] colors) {
        int n2copy = Math.min(theColors.length, colors.length);
        for (int i = 0; i < n2copy; ++i)
            theColors[i] = colors[i].getRGB();
    }

    int getDepth() {
        return colorDepth;
    }

    int getTransparent() {
        return transparentIndex;
    }

    void setTransparent(int color_index) {
        transparentIndex = color_index;
    }

    void processPixels(Gif89Frame gf) throws IOException {
        if (gf instanceof DirectGif89Frame)
            filterPixels((DirectGif89Frame) gf);
        else
            trackPixelUsage((IndexGif89Frame) gf);
    }

    void closePixelProcessing() {
        colorDepth = computeColorDepth(ciCount);
    }

    void encode(OutputStream os) throws IOException {
        int palette_size = 1 << colorDepth;
        for (int i = 0; i < palette_size; ++i) {
            os.write(theColors[i] >> 16 & 0xff);
            os.write(theColors[i] >> 8 & 0xff);
            os.write(theColors[i] & 0xff);
        }
    }

    private void filterPixels(DirectGif89Frame dgf) throws IOException {
        if (ciLookup == null)
            throw new IOException("RGB frames require palette autodetection");
        int[] argb_pixels = (int[]) dgf.getPixelSource();
        byte[] ci_pixels = dgf.getPixelSink();
        int npixels = argb_pixels.length;
        for (int i = 0; i < npixels; ++i) {
            int argb = argb_pixels[i];
            if ((argb >>> 24) < 0x80)
                if (transparentIndex == -1)
                    transparentIndex = ciCount;
                else if (argb != theColors[transparentIndex]) {
                    ci_pixels[i] = (byte) transparentIndex;
                    continue;
                }
            int color_index = ciLookup.getPaletteIndex(argb & 0xffffff);
            if (color_index == -1) {
                if (ciCount == 256)
                    throw new IOException("can't encode as GIF (> 256 colors)");
                theColors[ciCount] = argb;
                ciLookup.put(argb & 0xffffff, ciCount);
                ci_pixels[i] = (byte) ciCount;
                ++ciCount;
            }
            else
                ci_pixels[i] = (byte) color_index;
        }
    }

    private void trackPixelUsage(IndexGif89Frame igf) {
        byte[] ci_pixels = (byte[]) igf.getPixelSource();
        int npixels = ci_pixels.length;
        for (int i = 0; i < npixels; ++i)
            if (ci_pixels[i] >= ciCount)
                ciCount = ci_pixels[i] + 1;
    }

    private static int computeColorDepth(int colorcount) {
        if (colorcount <= 2)
            return 1;
        if (colorcount <= 4)
            return 2;
        if (colorcount <= 16)
            return 4;
        return 8;
    }
}

class ReverseColorMap {
    private static class ColorRecord {
        int rgb;
        int ipalette;

        ColorRecord(int rgb, int ipalette) {
            this.rgb = rgb;
            this.ipalette = ipalette;
        }
    }

    private static final int HCAPACITY = 2053;
    private ColorRecord[] hTable = new ColorRecord[HCAPACITY];

    int getPaletteIndex(int rgb) {
        ColorRecord rec;
        for (int itable = rgb % hTable.length;
             (rec = hTable[itable]) != null && rec.rgb != rgb;
             itable = ++itable % hTable.length
                )
            ;
        if (rec != null)
            return rec.ipalette;
        return -1;
    }


    void put(int rgb, int ipalette) {
        int itable;
        for (itable = rgb % hTable.length;
             hTable[itable] != null;
             itable = ++itable % hTable.length
                )
            ;
        hTable[itable] = new ColorRecord(rgb, ipalette);
    }
}

abstract class Gif89Frame {
    static final int DM_UNDEFINED = 0;
    static final int DM_LEAVE = 1;
    static final int DM_BGCOLOR = 2;
    static final int DM_REVERT = 3;
    int theWidth = -1;
    int theHeight = -1;
    byte[] ciPixels;

    private Point thePosition = new Point(0, 0);
    private boolean isInterlaced;
    private int csecsDelay;
    private int disposalCode = DM_LEAVE;

    void setPosition(Point p) {
        thePosition = new Point(p);
    }

    void setInterlaced(boolean b) {
        isInterlaced = b;
    }

    void setDelay(int interval) {
        csecsDelay = interval;
    }

    void setDisposalMode(int code) {
        disposalCode = code;
    }

    Gif89Frame() {
    }

    abstract Object getPixelSource();

    int getWidth() {
        return theWidth;
    }

    int getHeight() {
        return theHeight;
    }

    byte[] getPixelSink() {
        return ciPixels;
    }

    void encode(OutputStream os, boolean epluribus, int color_depth,
                int transparent_index) throws IOException {
        writeGraphicControlExtension(os, epluribus, transparent_index);
        writeImageDescriptor(os);
        new GifPixelsEncoder(
                theWidth, theHeight, ciPixels, isInterlaced, color_depth
        ).encode(os);
    }

    private void writeGraphicControlExtension(OutputStream os, boolean epluribus,
                                              int itransparent) throws IOException {
        int transflag = itransparent == -1 ? 0 : 1;
        if (transflag == 1 || epluribus) {
            os.write((int) '!');
            os.write(0xf9);
            os.write(4);
            os.write((disposalCode << 2) | transflag);
            Put.leShort(csecsDelay, os);
            os.write(itransparent);
            os.write(0);
        }
    }

    private void writeImageDescriptor(OutputStream os) throws IOException {
        os.write((int) ',');
        Put.leShort(thePosition.x, os);
        Put.leShort(thePosition.y, os);
        Put.leShort(theWidth, os);
        Put.leShort(theHeight, os);
        os.write(isInterlaced ? 0x40 : 0);
    }
}

class GifPixelsEncoder {
    private static final int EOF = -1;
    private int imgW, imgH;
    private byte[] pixAry;
    private boolean wantInterlaced;
    private int initCodeSize;
    private int countDown;
    private int xCur, yCur;
    private int curPass;

    GifPixelsEncoder(int width, int height, byte[] pixels, boolean interlaced,
                     int color_depth) {
        imgW = width;
        imgH = height;
        pixAry = pixels;
        wantInterlaced = interlaced;
        initCodeSize = Math.max(2, color_depth);
    }

    void encode(OutputStream os) throws IOException {
        os.write(initCodeSize);

        countDown = imgW * imgH;
        xCur = yCur = curPass = 0;

        compress(initCodeSize + 1, os);

        os.write(0);
    }

    private void bumpPosition() {
        ++xCur;
        if (xCur == imgW) {
            xCur = 0;
            if (!wantInterlaced)
                ++yCur;
            else
                switch (curPass) {
                    case 0:
                        yCur += 8;
                        if (yCur >= imgH) {
                            ++curPass;
                            yCur = 4;
                        }
                        break;
                    case 1:
                        yCur += 8;
                        if (yCur >= imgH) {
                            ++curPass;
                            yCur = 2;
                        }
                        break;
                    case 2:
                        yCur += 4;
                        if (yCur >= imgH) {
                            ++curPass;
                            yCur = 1;
                        }
                        break;
                    case 3:
                        yCur += 2;
                        break;
                }
        }
    }

    private int nextPixel() {
        if (countDown == 0)
            return EOF;
        --countDown;
        byte pix = pixAry[yCur * imgW + xCur];
        bumpPosition();
        return pix & 0xff;
    }

    static final int BITS = 12;
    static final int HSIZE = 5003;
    int n_bits;
    int maxbits = BITS;
    int maxcode;
    int maxmaxcode = 1 << BITS;

    static int MAXCODE(int n_bits) {
        return (1 << n_bits) - 1;
    }

    int[] htab = new int[HSIZE];
    int[] codetab = new int[HSIZE];
    int hsize = HSIZE;
    int free_ent = 0;
    boolean clear_flg = false;
    int g_init_bits;
    int ClearCode;
    int EOFCode;

    void compress(int init_bits, OutputStream outs) throws IOException {
        int fcode;
        int i /* = 0 */;
        int c;
        int ent;
        int disp;
        int hsize_reg;
        int hshift;
        g_init_bits = init_bits;
        clear_flg = false;
        n_bits = g_init_bits;
        maxcode = MAXCODE(n_bits);
        ClearCode = 1 << (init_bits - 1);
        EOFCode = ClearCode + 1;
        free_ent = ClearCode + 2;

        char_init();
        ent = nextPixel();
        hshift = 0;
        for (fcode = hsize; fcode < 65536; fcode *= 2)
            ++hshift;
        hshift = 8 - hshift;
        hsize_reg = hsize;
        cl_hash(hsize_reg);
        output(ClearCode, outs);
        outer_loop:
        while ((c = nextPixel()) != EOF) {
            fcode = (c << maxbits) + ent;
            i = (c << hshift) ^ ent;
            if (htab[i] == fcode) {
                ent = codetab[i];
                continue;
            }
            else if (htab[i] >= 0) {
                disp = hsize_reg - i;
                if (i == 0)
                    disp = 1;
                do {
                    if ((i -= disp) < 0)
                        i += hsize_reg;

                    if (htab[i] == fcode) {
                        ent = codetab[i];
                        continue outer_loop;
                    }
                }
                while (htab[i] >= 0);
            }
            output(ent, outs);
            ent = c;
            if (free_ent < maxmaxcode) {
                codetab[i] = free_ent++;
                htab[i] = fcode;
            }
            else
                cl_block(outs);
        }
        output(ent, outs);
        output(EOFCode, outs);
    }

    int cur_accum = 0;
    int cur_bits = 0;
    int masks[] = {0x0000, 0x0001, 0x0003, 0x0007, 0x000F,
            0x001F, 0x003F, 0x007F, 0x00FF,
            0x01FF, 0x03FF, 0x07FF, 0x0FFF,
            0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF};

    void output(int code, OutputStream outs) throws IOException {
        cur_accum &= masks[cur_bits];
        if (cur_bits > 0)
            cur_accum |= (code << cur_bits);
        else
            cur_accum = code;

        cur_bits += n_bits;

        while (cur_bits >= 8) {
            char_out((byte) (cur_accum & 0xff), outs);
            cur_accum >>= 8;
            cur_bits -= 8;
        }
        if (free_ent > maxcode || clear_flg) {
            if (clear_flg) {
                maxcode = MAXCODE(n_bits = g_init_bits);
                clear_flg = false;
            }
            else {
                ++n_bits;
                if (n_bits == maxbits)
                    maxcode = maxmaxcode;
                else
                    maxcode = MAXCODE(n_bits);
            }
        }
        if (code == EOFCode) {

            while (cur_bits > 0) {
                char_out((byte) (cur_accum & 0xff), outs);
                cur_accum >>= 8;
                cur_bits -= 8;
            }
            flush_char(outs);
        }
    }


    void cl_block(OutputStream outs) throws IOException {
        cl_hash(hsize);
        free_ent = ClearCode + 2;
        clear_flg = true;

        output(ClearCode, outs);
    }


    void cl_hash(int hsize) {
        for (int i = 0; i < hsize; ++i)
            htab[i] = -1;
    }

    int a_count;

    void char_init() {
        a_count = 0;
    }

    byte[] accum = new byte[256];

    void char_out(byte c, OutputStream outs) throws IOException {
        accum[a_count++] = c;
        if (a_count >= 254)
            flush_char(outs);
    }

    void flush_char(OutputStream outs) throws IOException {
        if (a_count > 0) {
            outs.write(a_count);
            outs.write(accum, 0, a_count);
            a_count = 0;
        }
    }
}

class IndexGif89Frame extends Gif89Frame {

    IndexGif89Frame(int width, int height, byte ci_pixels[]) {
        theWidth = width;
        theHeight = height;
        ciPixels = new byte[theWidth * theHeight];
        System.arraycopy(ci_pixels, 0, ciPixels, 0, ciPixels.length);
    }

    Object getPixelSource() {
        return ciPixels;
    }
}


final class Put {
    static void ascii(String s, OutputStream os) throws IOException {
        byte[] bytes = new byte[s.length()];
        for (int i = 0; i < bytes.length; ++i)
            bytes[i] = (byte) s.charAt(i);
        os.write(bytes);
    }

    static void leShort(int i16, OutputStream os) throws IOException {
        os.write(i16 & 0xff);
        os.write(i16 >> 8 & 0xff);
    }
}